package com.progenies.ecg.asm31.util;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import com.progenies.ecg.asm31.model.codeparts.IConfigurableCodePart;
import com.progenies.ecg.asm31.model.codeparts.lines.SetVariableValueLinePart;
import com.progenies.ecg.asm31.util.VariableRegister.Variable;
import com.progenies.ecg.model.AbstractCodeObject;
import com.progenies.ecg.model.ParameterObject;
import com.progenies.ecg.model.ValueReference;
import com.progenies.ecg.model.conditions.BooleanCondition;
import com.progenies.ecg.model.conditions.BooleanCondition.ConditionType;
import com.progenies.ecg.model.conditions.CombinationCondition;
import com.progenies.ecg.model.conditions.CombinationCondition.CombinationConditionType;
import com.progenies.ecg.model.conditions.ICondition;

/**
 * Helper class that will hold commons methods that generate bytecode instructions
 * @author ofc587a87
 *
 */
public final class BytecodeInstructionsUtils
{
	/** private constructor, no instantiation available */
	private BytecodeInstructionsUtils() {}
	
	
	
	public static void insertLocalVariableIntoStack(MethodVisitor visitor, VariableRegister varRegister, String name)
	{
		Variable thisVar=varRegister.getVariable(name);
		visitor.visitVarInsn(thisVar.getType().getOpcode(Opcodes.ILOAD), thisVar.getIndex());
	}
	
	
	public static Label createAndVisitLabel(MethodVisitor visitor)
	{
		Label lbl=new Label();
		visitor.visitLabel(lbl);
		return lbl;
	}
	
	
	public static void createNewObjectIntoStack(MethodVisitor visitor, Type objectType, ParameterObject<?> parameters[], VariableRegister varRegister)
	{
		//creates new object and store in stack
		visitor.visitTypeInsn(Opcodes.NEW, objectType.getInternalName());
		//duplicates the stack
		visitor.visitInsn(Opcodes.DUP);
		
		//if there are parameters, resolve them
		if(parameters!=null && parameters.length>0)
		{
			for(int i=0;i<parameters.length;i++)
			{
				if(parameters[i].getValue()!=null && !parameters[i].getValue().equals(SetVariableValueLinePart.FORZED_NULL))
					insertValueIntoStack(visitor, parameters[i].getValue());
				else if(parameters[i].getVariableName()!=null)
					insertLocalVariableIntoStack(visitor, varRegister, parameters[i].getVariableName());
				else //null value
					insertValueIntoStack(visitor, null);
			}
		}
		
		//remove numParameters object from stack, remove another object  from the stack (object instance) and calls constructor on it
		visitor.visitMethodInsn(Opcodes.INVOKESPECIAL, objectType.getInternalName(), "<init>", GenerationUtils.translateDescription(parameters,  null)); 
	}
	
	public static void createNewArrayIntoStack(MethodVisitor visitor, Type arrayType, int arraySize[])
	{	
		//insert into stack the array dimension (for multidimensional arrays, insert each dimension)
		for(int dim : arraySize)
		{
			insertValueIntoStack(visitor, dim);
		}
				
//				System.out.println("Dimensions of matrix: "+arraySize.length);
		
		//creates new array
		if(arrayType.getSort() == Type.ARRAY && arraySize.length>1)
			visitor.visitMultiANewArrayInsn(arrayType.getDescriptor(), arraySize.length);
		else if(arrayType.getElementType().getSort() == Type.OBJECT) //array of object
			visitor.visitTypeInsn(Opcodes.ANEWARRAY, arrayType.getElementType().getInternalName());
		else //array of primitives
			visitor.visitIntInsn(Opcodes.NEWARRAY, arrayType.getElementType().getOpcode(Opcodes.T_INT));
	}
	
	
	public static void insertValueIntoStack(MethodVisitor visitor, Object value)
	{
		if(value==null)
			visitor.visitInsn(Opcodes.ACONST_NULL);
		if(value instanceof Integer)
			insertIntIntoStack(visitor, (Integer) value);
		else if(value instanceof Boolean)
			insertBooleanIntoStack(visitor, (Boolean) value);
		else if(value instanceof Long)
			insertLongIntoStack(visitor, (Long) value);
		else if(value instanceof Double)
			insertDoubleIntoStack(visitor, (Double) value);
		else if(value instanceof Float)
			insertFloatIntoStack(visitor, (Float) value);
		else
			visitor.visitLdcInsn(value);
	}
	
	
	public static void insertIntIntoStack(MethodVisitor visitor, int dim)
	{
		if(dim<0 || dim>5)
			visitor.visitLdcInsn(dim);
		else
		{
			int resultCode=-1;
			switch(dim)
			{
				case 0:
					resultCode=Opcodes.ICONST_0;
					break;
				case 1:
					resultCode=Opcodes.ICONST_1;
					break;
				case 2:
					resultCode=Opcodes.ICONST_2;
					break;
				case 3:
					resultCode=Opcodes.ICONST_3;
					break;
				case 4:
					resultCode=Opcodes.ICONST_4;
					break;
				case 5:
					resultCode=Opcodes.ICONST_5;
					break;
				default:
					throw new RuntimeException("Unknown error, please contact developer"); //Should NEVER reach here
			}
			visitor.visitInsn(resultCode);
		}
		
	}


	public static void insertBooleanIntoStack(MethodVisitor visitor, boolean val)
	{
		visitor.visitInsn(val?Opcodes.ICONST_1:Opcodes.ICONST_0);
	}
	
	public static void insertLongIntoStack(MethodVisitor visitor, long val)
	{
		if(val==0l)
			visitor.visitInsn(Opcodes.LCONST_0);
		else if(val==1l)
			visitor.visitInsn(Opcodes.LCONST_1);
		else
			visitor.visitLdcInsn(val);
	}
	
	public static void insertDoubleIntoStack(MethodVisitor visitor, double val)
	{
		if(val==0d)
			visitor.visitInsn(Opcodes.DCONST_0);
		else if(val==1d)
			visitor.visitInsn(Opcodes.DCONST_1);
		else
			visitor.visitLdcInsn(val);
	}
	
	public static void insertFloatIntoStack(MethodVisitor visitor, float val)
	{
		if(val==0f)
			visitor.visitInsn(Opcodes.FCONST_0);
		else if(val==1f)
			visitor.visitInsn(Opcodes.FCONST_1);
		else if(val==2f)
			visitor.visitInsn(Opcodes.FCONST_2);
		else
			visitor.visitLdcInsn(val);
	}


	public static <T> void createNewArrayIntoStackAndPopulate(MethodVisitor visitor, Type arrayType, T value)
	{
		ArrayList<Integer> list=new ArrayList<Integer>();
		Object tmpValue=value;
		while(tmpValue.getClass().isArray())
		{
			list.add(Array.getLength(tmpValue));
			tmpValue=Array.get(tmpValue, 0);
		}
		
		int arrDimensions[]=new int[list.size()];
		for(int i=0;i<list.size();i++)
			arrDimensions[i]=list.get(i).intValue();
		
		createNewArrayIntoStack(visitor, arrayType, arrDimensions);
		 
		//populate with data
		for(int i=0;i<arrDimensions[0];i++)
		{
			visitor.visitInsn(Opcodes.DUP); //duplicates the array reference (will be consumed)
			
			if(value!=null)
			{
				//if the value is an array (array of arrays) i'll  recursively create it
				Object newValue=Array.get(value, i);
				String descString=arrayType.getDescriptor();
				if(descString.startsWith("["))
					descString=descString.substring(1);

				BytecodeInstructionsUtils.insertValueIntoStackArray(visitor, Type.getType(descString), i, newValue);
			}
			else // The desired value is NULL
				visitor.visitInsn(Opcodes.ACONST_NULL);
		}
		
	}
	
	
	public static void createNewArrayIntoStackAndPopulate(MethodVisitor visitor, Type arrayType,  ValueReference<?>[] arrVarReference, AbstractCodeObject parentObject, VariableRegister varRegister)
	{
		int arrLength=arrVarReference.length;
		
		 createNewArrayIntoStack(visitor, arrayType, new int[] {arrLength});
		 
		//populate with data
		for(int i=0;i<arrLength;i++)
		{
			visitor.visitInsn(Opcodes.DUP); //duplicates the array reference (will be consumed)
			
			if(arrVarReference[i].getName()!=null)
				BytecodeInstructionsUtils.insertVariableValueIntoStackArray(visitor, arrayType, i, varRegister, parentObject, arrVarReference[i]);
			else if(arrVarReference[i].getStaticValue()!=null)
			{
				Object value=arrVarReference[i].getStaticValue();
				Type targetType=value.getClass().isArray()?arrayType:arrayType.getElementType();
				while(targetType.getDescriptor().lastIndexOf("[")>Type.getDescriptor(value.getClass()).lastIndexOf("["))
					targetType=Type.getType(targetType.getDescriptor().substring(1));
				
//				System.out.println("Target: "+targetType+", value: "+Type.getDescriptor(value.getClass()));
				BytecodeInstructionsUtils.insertValueIntoStackArray(visitor, targetType, i, value);
			} //TODO: Support other types of ValueReference
			else // The desired value is NULL 
				visitor.visitInsn(Opcodes.ACONST_NULL);
		}
		
	}
	
	public static void createNewArrayIntoStackAndPopulate(MethodVisitor visitor, Type arrayType,  ValueReference<?>[][] arrVarReference, AbstractCodeObject parentObject, VariableRegister varRegister)
	{
		int arrLength=arrVarReference.length;
		int arrLength2=arrVarReference[0].length;
		
		 createNewArrayIntoStack(visitor, arrayType, new int[] {arrLength, arrLength2});
		 
		//populate with data
		for(int i=0;i<arrLength;i++)
		{
			visitor.visitInsn(Opcodes.DUP); //duplicates the array reference (will be consumed)
			
			insertValueIntoStack(visitor, i);
			createNewArrayIntoStackAndPopulate(visitor, arrayType, arrVarReference[i], parentObject, varRegister);
			visitor.visitInsn(Opcodes.AASTORE);
		}
		
	}
	
	/**
	 * Assumes the array is the last reference in the stack
	 * @param visitor
	 * @param index
	 * @param value
	 */
	public static void insertValueIntoStackArray(MethodVisitor visitor, Type arrayType, int index, Object value)
	{
		//inserts the index into stack
		insertValueIntoStack(visitor, index);
		
		//inserts the value into stack. If it's an array, i'll recursively insert it
		if(value.getClass().isArray())
			createNewArrayIntoStackAndPopulate(visitor, arrayType, value);
		else
			insertValueIntoStack(visitor, value);
		
		Type targetType=arrayType;
		if(arrayType.getSort()==Type.ARRAY && value.getClass().isArray())
		{
			while(targetType.getDescriptor().lastIndexOf("[")>Type.getDescriptor(value.getClass()).lastIndexOf("["))
				targetType=Type.getType(targetType.getDescriptor().substring(1));
		}
		
		//Conversion needed?
		if(!value.getClass().isArray()) //no conversion for arrays
		{	
			Type originType=Type.getType(value.getClass().isArray()?value.getClass().getComponentType():value.getClass());
			int conversionCode=GenerationUtils.getCastNeeded(originType, targetType);
			if(conversionCode!=Opcodes.NOP)
			{
				if(conversionCode==Opcodes.CHECKCAST)
				{
					visitor.visitTypeInsn(conversionCode, targetType.getInternalName());
				}
				else
					visitor.visitInsn(conversionCode);
			}
		}
		
		//inserts the value into the array (and removes array, index and object from stack)
		visitor.visitInsn(targetType.getSort()==Type.ARRAY?Opcodes.AASTORE:targetType.getOpcode(Opcodes.IASTORE));
	}
	

	public static void insertVariableValueIntoStackArray(MethodVisitor visitor,	Type arrayType, int index,
						VariableRegister varRegister, AbstractCodeObject parent, ValueReference<?> varReference) 
	{
		//TODO: Check ValueReference use, only getName and OwnerVariable admitted here.
		
		//inserts the index into stack
		insertIntIntoStack(visitor, index);
		
		//type of the value to insert (for conversion checking)
		Type originType=null;
		
		//inserts the value into stack
		//if it's an static field of an external class, load it
		if(varReference.getOwnerClass()!=null)
		{
			try
			{
				originType=Type.getType(varReference.getOwnerClass().getField(varReference.getName()).getType());
				visitor.visitFieldInsn(Opcodes.GETSTATIC, GenerationUtils.translateClassName(varReference.getOwnerClass().getName()),
					varReference.getName(), originType.getDescriptor());
			}catch(Exception ex)
			{
				throw new RuntimeException("Error populating array value", ex);
			}
		}
		else //it's a variable, a field, or a field from a variable object. I'll load it into stack anyway
		{
			String variableLevel0=varReference.getOwnerVariable()==null?varReference.getName():varReference.getOwnerVariable();
			String variableLevel1=varReference.getOwnerVariable()==null?null:varReference.getName();
			
//			System.out.println("Levels: 0->"+variableLevel0+" / 1-> "+variableLevel1);
			
			//loads variable or it's parent into stack
			Variable var=varRegister.getVariable(variableLevel0);
			if(var.isLocal())
				insertLocalVariableIntoStack(visitor, varRegister, variableLevel0);
			else //instance or static field
			{
				if(!var.isStatic())
					insertLocalVariableIntoStack(visitor, varRegister, "this");
				
				visitor.visitFieldInsn(var.isStatic()?Opcodes.GETSTATIC:Opcodes.GETFIELD, GenerationUtils.getFieldOwner(variableLevel0, parent), variableLevel0, var.getType().getDescriptor());
			}
			
			//if its a field from the object loaded into stack, get it
			if(variableLevel1!=null)
			{
				try {
					originType=Type.getType(Class.forName(var.getType().getClassName()).getField(variableLevel1).getType());
				} catch (Exception e)
				{
					throw new RuntimeException("Class of the variable not found!", e);
				}
				
				//it's NOT static (i have received a variable name that is now loaded into stack)
				visitor.visitFieldInsn(Opcodes.GETFIELD, var.getType().getInternalName(), variableLevel1, originType.getDescriptor());
				
			}
			else
				originType=var.getType(); //the type was the variable type
		}
		
		Type targetType=arrayType.getElementType();
		
		//conversion needed?
		int conversionCode=GenerationUtils.getCastNeeded(originType, targetType);
		if(conversionCode!=Opcodes.NOP)
		{
			if(conversionCode==Opcodes.CHECKCAST)
				visitor.visitTypeInsn(conversionCode, arrayType.getInternalName());
			else
				visitor.visitInsn(conversionCode);
		}
		
		//inserts the value into the array (and removes array, index and object from stack)
		visitor.visitInsn(arrayType.getElementType().getOpcode(Opcodes.IASTORE));
	}

	
	
	public static int getReturnOpcodeForType(Object value, boolean wrapPrimitives)
	{
		if(value==null || wrapPrimitives) //if wrap primitives, they will be always objects!!
			return Opcodes.ARETURN;
		
		if(value instanceof Boolean || value instanceof Integer
				|| value instanceof Byte || value instanceof Character || value instanceof Short )
			return Opcodes.IRETURN;
		
		
		if(value instanceof Long)
			return Opcodes.LRETURN;
		
		if(value instanceof Float)
			return Opcodes.FRETURN;
		
		if(value instanceof Double)
			return Opcodes.DRETURN;

		//defaults to Object
		return Opcodes.ARETURN;
	}



	public static void evaluateCondition(MethodVisitor visitor, ICondition condition, Label failLabel, VariableRegister varRegister, AbstractCodeObject parent)
	{
		if(condition instanceof BooleanCondition<?>)
		{
			BooleanCondition<?> cond=(BooleanCondition<?>)condition;
			
			//evaluate ValueReference of first operand and insert value into stack
			insertValueReferenceIntoStack(visitor, cond.getConditionValue(), varRegister, parent);
			
			if(!cond.getConditionType().isSingleValue())
			{
				//for comparisons, insert the second value into stack
				insertValueReferenceIntoStack(visitor, cond.getSecundaryConditionValue(), varRegister, parent);
			}
			
			//now, execute the evaluation and jump if fails
			int opcode=getJumpOpcode(cond.getConditionType());
			
			visitor.visitJumpInsn(opcode, failLabel);
		}
		else if(condition instanceof CombinationCondition)
		{
			CombinationCondition comb=(CombinationCondition)condition;
			
			//evaluate left operand. If the combination is And and if fails, there is no need to evaluate the second one
			Label failFirstCondition=null;
			if(CombinationConditionType.AND==comb.getConditionType())
				failFirstCondition=failLabel;
			else
				failFirstCondition=new Label();
			evaluateCondition(visitor, comb.getConditionCodeLeft(), failFirstCondition, varRegister, parent);
			
			//Label for OR combination
			Label startBodyLabel=null;
			if(CombinationConditionType.OR==comb.getConditionType())
			{
				//if it's an OR, if the first evaluations was true, there is no need for the second one, so GOTO body start
				startBodyLabel=new Label();
				visitor.visitJumpInsn(Opcodes.GOTO, startBodyLabel);
				
				//if the first condition was false, it'll jump to this label
				visitor.visitLabel(failFirstCondition);
			}
			
			//evaluates second condition, goes to failLabel if it fails, both for AND and OR combinations
			evaluateCondition(visitor, comb.getConditionCodeRight(), failLabel, varRegister, parent);
			
			if(startBodyLabel!=null)
				visitor.visitLabel(startBodyLabel);
		}
		else
			throw new RuntimeException("ICondition not supported!!");
	}



	private static int getJumpOpcode(ConditionType conditionType) {
		switch(conditionType)
		{
			//BEWARE: The condition evaluation is INVERSE: jumps to else block if the condition evaluate to true.
			//         In Java, it jumps if the condition evaluate to false
		
			//single value conditions
			case IS_TRUE:  		return Opcodes.IFEQ;  //jumps if value==0 (jumps if false) 
			case IS_FALSE: 		return Opcodes.IFNE;  //jumps if value!=0 (jumps if true)
			case IS_NULL:  		return Opcodes.IFNONNULL; //jumps if value is not null 
			case IS_NOT_NULL:	return Opcodes.IFNULL;    //jumps if value is null
				
			//double values comparisons
			case IS_GREATER_THAN:			return Opcodes.IF_ICMPLE; //jumps if A <= B 
			case IS_GREATER_OR_EQUALS_TO:	return Opcodes.IF_ICMPLT; //jumps if A <  B
			case IS_LESS_THAN:				return Opcodes.IF_ICMPGE; //Jumps if A >= B
			case IS_LESS_OR_EQUALS_TO:		return Opcodes.IF_ICMPGT; //jumps if A >  B
			case IS_EQUALS_TO:				return Opcodes.IF_ICMPNE; //jumps if A != B
			case IS_NOT_EQUALS_TO:			return Opcodes.IF_ICMPEQ; //jumps if A == B
			
			//TODO: comparisons of objects IF_ACMPxx
			default:
				return Opcodes.NOP; //it will fail
		}
	}



	public static Type insertValueReferenceIntoStack(MethodVisitor visitor, ValueReference<?> valueReference, VariableRegister varRegister, AbstractCodeObject parent)
	{
		Type resultType=null;
		if(valueReference.getStaticValue()!=null)
		{
			Object obj=valueReference.getStaticValue();
			insertValueIntoStack(visitor, obj);
			resultType=Type.getType(obj.getClass());
			
		}
		else if(valueReference.getOwnerClass()!=null)
		{
			try
			{
				resultType=Type.getType(valueReference.getOwnerClass().getField(valueReference.getName()).getType());
				visitor.visitFieldInsn(Opcodes.GETSTATIC, GenerationUtils.translateClassName(valueReference.getOwnerClass().getName()),
						valueReference.getName(), resultType.getDescriptor());
			}catch(Exception ex)
			{
				throw new RuntimeException("Error inserting value into stack", ex);
			}
		}
		else if(valueReference.getOwnerVariable()!=null)
		{
			Variable var=varRegister.getVariable(valueReference.getOwnerVariable());
			Field fieldObj=null;
			try {
				fieldObj = Class.forName(var.getType().getClassName()).getField(valueReference.getName());
			}catch(Exception ex)
			{
				throw new RuntimeException("Error inserting value into stack", ex);
			}
			Type varType=Type.getType(fieldObj.getType());

			
			if(var.isLocal())
				insertLocalVariableIntoStack(visitor, varRegister, var.getName());
			else
			{
				if(!var.isStatic())
					insertLocalVariableIntoStack(visitor, varRegister, "this");
				visitor.visitFieldInsn(var.isStatic()?Opcodes.GETSTATIC:Opcodes.GETFIELD, GenerationUtils.getFieldOwner(var.getName(), parent), var.getName(), var.getType().getDescriptor());
			}
			
			boolean isStatic=Modifier.isStatic(fieldObj.getModifiers());
			
			visitor.visitFieldInsn(isStatic?Opcodes.GETSTATIC:Opcodes.GETFIELD, var.getType().getInternalName(), valueReference.getName(), varType.getDescriptor());
			resultType=varType;
		}
		else if(valueReference.getName()!=null)
		{
			Variable var=varRegister.getVariable(valueReference.getName());
			if(var.isLocal())
				insertLocalVariableIntoStack(visitor, varRegister, valueReference.getName());
			else
			{
				if(!var.isStatic())
					insertLocalVariableIntoStack(visitor, varRegister, "this");
				visitor.visitFieldInsn(var.isStatic()?Opcodes.GETSTATIC:Opcodes.GETFIELD, GenerationUtils.getFieldOwner(var.getName(), parent), var.getName(), var.getType().getDescriptor());
			}
			resultType=var.getType();
		}
		else if(valueReference.getExecutionLine()!=null)
		{
			if(valueReference.getExecutionLine() instanceof IConfigurableCodePart)
				((IConfigurableCodePart)valueReference.getExecutionLine()).configure(visitor, parent, varRegister);
			valueReference.getExecutionLine().generateBytecode();
			
			//TODO: Can i get resultType?
		}
		else //NULL value
		{
			visitor.visitInsn(Opcodes.ACONST_NULL);
			//TODO: Can i get resultType?
		}
		
		return resultType;

	}



	public static void checkAndConvertValue(MethodVisitor visitor, Type originType, Type varType, boolean automaticConversion) {
		int conversionOpcode=GenerationUtils.getCastNeeded(originType, varType);
		if(!automaticConversion && conversionOpcode!=Opcodes.NOP)
		{
			//Error, not automatic conversion and it was needed!!
			throw new RuntimeException("Conversion needed but not enabled");
		}
		else if(automaticConversion && conversionOpcode!=Opcodes.NOP) //if needed and enabled...
		{
			if(conversionOpcode==Opcodes.CHECKCAST) //object casting
				visitor.visitTypeInsn(conversionOpcode, varType.getInternalName());
			else
				visitor.visitInsn(conversionOpcode); //primitive conversion
		}
	}



	public static void insertArrayElementIntoStack(MethodVisitor visitor, ValueReference<Object> arrayVariable, ValueReference<Object> indexVariable, VariableRegister varRegister, AbstractCodeObject parent)
	{
		//insert array into stack
		Type arrType=insertValueReferenceIntoStack(visitor, arrayVariable, varRegister, parent);
		
		//insert index into stack
		insertValueReferenceIntoStack(visitor, indexVariable, varRegister, parent);
		
		//resolve element
		visitor.visitInsn(arrType.getElementType().getOpcode(Opcodes.IALOAD));
	}







}
